<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>开心消消乐</title>
		<link rel="shortcut icon" href="./logo.png" type="image/x-icon">
		<link rel="stylesheet" href="./index.css">
		<script src="./vue.min.js"></script>
	</head>
	<body>
		<!-- 界面 -->
		<div id="app">
			<div class="title">开心消消乐</div>
			<div class="score">分数：{{ score }}</div>
			<div class="game">
				<div class="game-map">
					<!-- 绘制棋盘 -->
					<div v-for="(item1, index1) in gameList" :key="index1" class="game-row">
						<div
							v-for="(item2, index2) in item1"
							:key="index2"
							class="game-col"
							:style="{'width': blockSize + 'px', 'height': blockSize + 'px' }"
						></div>
					</div>
					<!-- 图片定位 -->
					<div
						v-for="(item, index) in gameListData"
						:key="index"
						class="game-spirit"
						:style="{
							width: blockSize + 'px',
							height: blockSize + 'px',
							opacity: item.show ? 1 : 0,
							transform: `translate(${item.left}px, ${item.top}px)`,
							transition: isDown ? `transform ${downSleep}s` : (!item.show ? `opacity ${downSleep}s` : '')
						}"
						:class="{
							'selected': item?.left === selectItem?.left && item?.top === selectItem?.top
						}"
						@click="clickItem(item)"
					>
						<img
							v-if="item?.type"
							:src="`./img/${item.type}.png`"
							alt=""
							:class="{
								'leftAndRight': item.option === 1,
								'topAndBottom': item.option === 2,
								'roundAmazing': item.option === 3,
								'allAmazing': item.option === 4
							}"
						>
						<div class="mask"></div>
					</div>
				</div>
			</div>
		</div>

		<!-- 逻辑 -->
		<script>
			const vm = new Vue({
				el: '#app',
				data() {
					return {
						row: 8,
						col: 8,
						blockSize: 60, // 块的大小
						levelList: [], // 方块动画等级
						level: 6,
						gameList: [], // 存放数据的二维数组
						gameListData: [], // 存放数据一维数组
						typeList: ['葡萄', '苹果', '蓝莓', '桃子', '杨桃'], // '香蕉', '草莓', '橙子', '菠萝', '山竹', '葡萄', '苹果', '蓝莓', '桃子', '杨桃', '猕猴桃', '柠檬'
						selectItem: null,
						isChange: false, // 两个方块交换过程控制
						isDown: false, // 下落过程控制
						downSleep: 0.3, // 动画速度，秒
						score: 0
					}
				},
				created() {
					this.initLevel()
					this.initGame()
				},
				methods: {
					// 生成游戏地图
					initGame() {
						for (let i = 0; i < this.row; i++) {
							this.gameList[i] = []
							for (let j = 0; j < this.col; j++) {
								this.gameList[i][j] = {
									type: this.typeList[Math.floor(Math.random() * this.typeList.length - 1) + 1], // 方块存放的类型
									left: j * this.blockSize,
									top: i * this.blockSize,
									show: true,
									option: 0 // 功能类型 0=默认 1=左右 2=上下 3=范围 4=全屏
								}
							}
						}
						this.gameListData = [].concat(...this.gameList)
						setTimeout(() => {
							this.clearBlock()
						}, 100)
					},

					// 生成动画运动等级
					initLevel() {
						const factors = [];
						for (let i = 1; i <= this.blockSize; i++) {
							if (this.blockSize % i === 0) {
								factors.push(i);
							}
						}
						this.levelList = factors.map((item, index) => {
							return {
								level: index + 1,
								value: item
							}
						})
					},

					// 选择方块
					clickItem(item) {
						// 如果正在交换/下落中，请勿打扰
						if (this.isChange || this.isDown) {
							return
						}
						// 如果选中的块或者被选中的块是空的，什么都没发生
						if (!item.show || (this.selectItem && !this.selectItem.show)) {
							return
						}
						// 再一次选择了我自己，什么都没发生
						if (this.selectItem && this.selectItem.left === item.left && this.selectItem.top === item.top) {
							return
						}
						// 选中
						if (!this.selectItem) {
							// 如果没选择，则需要赋值
							this.selectItem = item
							return
						}
						// 如果选择了，则需要判断第二个选择的是否在上下左右中
						if (
							!(Math.abs(item.left - this.selectItem.left) === this.blockSize && item.top === this.selectItem.top) &&
							!(Math.abs(item.top - this.selectItem.top) === this.blockSize && item.left === this.selectItem.left)
						) {
							this.selectItem = item
							return
						}
						// 交换两个方块的位置
						// item.type === this.selectItem.type 如果选中的两个是相同的方块，则不需要替换
						this.changeBlockPositon(item, this.selectItem, this.level, () => {
							// 判断是否有清空
							if (!this.clearBlock()) {
								this.changeBlockPositon(this.selectItem, item, this.level, () => {
									this.selectItem = null
									this.isChange = false
									// console.log('----动画执行完了222', this.selectItem, this.isChange)
								})
							} else {
								this.selectItem = null
								this.isChange = false
								// console.log('----动画执行完了111', this.selectItem, this.isChange)
							}
						})
					},

					// 更换两个方块的位置
					// 这里使用 requestAnimationFrame 做方块移动动画效果
					// selectItem 选择的第一个
					// currentItem 选择的第二个
					// level 运行速度等级 等级越高速度越快
					changeBlockPositon(currentItem, selectItem, level = this.level, callback) {
						this.isChange = true
						// 移动量
						const duration = this.levelList.find(item => item.level === level)?.value ?? this.levelList[Math.floor(this.levelList.length/2) + 1].value ?? 10
						// 控制移动变量
						let controlSize = 0
						// 移动方向
						let direction = ''
						if (currentItem.left === selectItem.left && currentItem.top < selectItem.top) { direction = 'up' }
						if (currentItem.left === selectItem.left && currentItem.top > selectItem.top) { direction = 'down' }
						if (currentItem.top === selectItem.top && currentItem.left < selectItem.left) { direction = 'left' }
						if (currentItem.top === selectItem.top && currentItem.left > selectItem.left) { direction = 'right' }
						// 两个方块的下标
						const selectIndex = this.gameListData.findIndex(item => item.left === selectItem.left && item.top === selectItem.top)
						const currentIndex = this.gameListData.findIndex(item => item.left === currentItem.left && item.top === currentItem.top)
						// 计算两个方块之间的差值
						const diffVal = direction === 'up' || direction === 'down' ? Math.abs(currentItem.top - selectItem.top) : Math.abs(currentItem.left - selectItem.left)
						// 移动动画
						const animate = (timestamp) => {
							// 上
							if (direction === 'up') {
								this.gameListData[selectIndex].top -= duration
								this.gameListData[currentIndex].top += duration
							}
							// 下
							if (direction === 'down') {
								this.gameListData[selectIndex].top += duration
								this.gameListData[currentIndex].top -= duration
							}
							// 左
							if (direction === 'left') {
								this.gameListData[selectIndex].left -= duration
								this.gameListData[currentIndex].left += duration
							}
							// 右
							if (direction === 'right') {
								this.gameListData[selectIndex].left += duration
								this.gameListData[currentIndex].left -= duration
							}
							// 每次移动
							controlSize += duration
							// console.log('----1', direction, '1-----', Math.abs(controlSize), this.gameListData[selectIndex].top, this.gameListData[currentIndex].top)
							if (Math.abs(controlSize) < diffVal) {
								requestAnimationFrame(animate);
							} else {
								// 动画完成，执行回调
								if (typeof callback === 'function') {
									callback();
								} else {
									this.isChange = false
									return
								}
							}
						}
						requestAnimationFrame(animate);
					},

					// 消除方块
					// 是否有相连的三颗及以上
					//    1.1 - 三颗直接消除
					//    1.2 - 四颗直接消除 - 变成技能方块
					//        1.2.1 - 横向四颗合成 - 消除一行
					//        1.2.2 - 纵向四颗合成 - 消除一列
					//        1.2.3 - T字形、L型合成 - 消除范围
					//    1.3 - 五颗以上 - 变成全能方块
					// return 本次是否消除了方块
					clearBlock() {
						// 二维数组，存放所有能消除的
						let cleanList = []
						const fallOldList = []
						// 循环搜查所有方块
						for (let i = 0; i < this.gameListData.length; i++) {
							const item = this.gameListData[i]
							// 如果有方块在地图上面，则清空
							if (item.top < 0) {
								fallOldList.push(item)
							} else {
								// 向右查找
								const list_right = [item]
								for (let j = 1; j < 5; j++) {
									const left = item.left + j * this.blockSize
									const top = item.top
									const nextItem = this.gameListData.find(f => f.left === left && f.top === top)
									// 判断边界
									if (nextItem && nextItem.show && nextItem.type === item.type) {
										list_right.push(nextItem)
									} else {
										break
									}
								}
								// 向下查找
								const list_bottom = [item]
								for (let j = 1; j < 5; j++) {
									const left = item.left
									const top = item.top + j * this.blockSize
									const nextItem = this.gameListData.find(f => f.left === left && f.top === top)
									// 判断边界
									if (nextItem && nextItem.show && nextItem.type === item.type) {
										list_bottom.push(nextItem)
									} else {
										break
									}
								}
								
								// 判断一下是否可以消除
								if (list_right.length > 2) {
									cleanList.push(list_right)
									this.score++
								}
								if (list_bottom.length > 2) {
									cleanList.push(list_bottom)
									this.score++
								}
							}
						}
						
						// 如果fallOldList
						if (fallOldList.length) {
							fallOldList.forEach(item => {
								const index = this.gameListData.findIndex(a => a.top === item.top && a.left === item.left)
								this.gameListData.splice(index, 1)
							})
						}

						// 如果有可以清空的数据
						// console.log('---是否有消除方块', cleanList.length > 0)
						if (cleanList.length) {
							// 首先需要剔除重复计算的数据，比如有一个四连的，则会被计算进去两次数组，分别为长度为4的数组和长度为3的数据，以此类推，五连会计算三个数组，5、4、3长度
							// 所以只要计算有两个消除组中包含同一个方块，则说明他们是包含或者相连的，这时候合并去重即可（会形成 L T等形状的消除块）
							const cleanListFlat = cleanList.flat() // 变成一维数组
							const cleanListFilter = [] // 去重的一维数组
							// 去重
							const uniqueSet = new Set()
							for (let i = 0; i < cleanListFlat.length; i++) {
								const item = cleanListFlat[i]
								const key = `${item.type}-${item.left}-${item.top}`
								if (!uniqueSet.has(key)) {
									uniqueSet.add(key)
									cleanListFilter.push(item)
								}
							}
							// 这个是记录方块组。用于后期消除时，重新生成功能方块
							const cleanListFilterTwo = this.mergeAndDeduplicateGroups(cleanList)
							console.log('---消除方块', cleanListFilterTwo)
							// 清除方块
							// option功能类型 0=无 1=左右 2=上下 3=范围 4=全屏
							cleanListFilterTwo.forEach(item1 => {
								// 判断第一块和第二块的left是否相等，如果相等则是上下，不相等则是左右
								const isLeft = item1[0].left === item1[1].left
								// 如果是五个的话，判断5个是不是一条线，判断所有块的left值或者top值是否一样
								let isFive = false
								if (item1.length === 5) { 
									const allLeftEqual = item1.every((item, index, arr) => item.left === arr[0].left)
									const allTopEqual = item1.every((item, index, arr) => item.top === arr[0].top)
									isFive = allLeftEqual || allTopEqual
								}
								item1.forEach((item2, index2) => {
									const index = this.gameListData.findIndex(f => f.left === item2.left && f.top === item2.top)
									// 第一块不用消失，直接设置为功能方块即可
									if (index2 === 0 && item1.length > 3) {
										this.gameListData[index].option = item1.length === 4 ? (isLeft ? 2 : 1) : (isFive ? 4 : 3)
									} else {
										// 消除的时候，需要知道消除的方块有没有功能方块
										this.abilityClean(item2, index)
										// this.gameListData[index].show = false
										// this.gameListData[index].option = 0
									}
								})
							})
							// 消除完，重新在地图的上方生成新的方块
							console.log('-----')
							// （1）先给同一列的空块分为一组
							const gameListData = JSON.parse(JSON.stringify(this.gameListData))
							const allNullMap = new Map()
							gameListData.forEach(item => {
								const left = item.left
								if (!item.show) {
									if(!allNullMap.has(left)) {
										allNullMap.set(left, [item])
									} else {
										allNullMap.set(left, [...allNullMap.get(left), item])
									}
								}
							})
							const allNullList = Array.from(allNullMap.values())
							console.log('-----空方块', allNullList)
							// （2）生成新的方块，放到地图的上方用于下落使用
							for (let i = 0; i < allNullList.length; i++) {
								for (let j = 0; j < allNullList[i].length; j++) {
									const newItem = {
										...allNullList[i][j],
										top: 0 - (j + 1) * this.blockSize,
										type: this.typeList[Math.floor(Math.random() * this.typeList.length - 1) + 1],
										show: true,
										option: 0
									}
									console.log('-----产生的新方块', newItem)
									this.gameListData.push(newItem)
								}
							}
							// 地图重绘，方块下落
							this.drawMap(cleanListFilter)
						}
						// 是否有清空
						return cleanList.length > 0
					},
					
					// 递归处理功能方块
					// 例如清除一行之后，一行中可能包含其他的功能方块，所以我们需要一直清除
					abilityClean(item, parentAtIndex) {
						if (item.option === 1) {
							// 左右一行清除，只要找到top值一样的即可
							const list1 = this.gameListData.filter(a => a.top === item.top)
							list1.forEach(item1 => {
								const item1Copy = JSON.parse(JSON.stringify(item1))
								const index = this.gameListData.findIndex(a => a.left === item1.left && a.top === item1.top)
								this.gameListData[index].show = false
								this.gameListData[index].option = 0
								// 如果找到的这个是个功能方块，则继续处理
								if (item1Copy.option !== 0) {
									// this.abilityClean(item1Copy, parentAtIndex)
								}
							})
						} else if (item.option === 2) {
							// 上下同理
							const list2 = this.gameListData.filter(a => a.left === item.left)
							list2.forEach(item1 => {
								const item1Copy = JSON.parse(JSON.stringify(item1))
								item1.show = false
								item1.option = 0
								// 如果找到的这个是个功能方块，则继续处理
								if (item1Copy.option !== 0) {
									// this.abilityClean(item1Copy, parentAtIndex)
								}
							})
						} else if (item.option === 3) {
							// 范围消除，规定一下，需要清除周围八个以及每边中心点外的第二个
							const cleanBlockList = [
								{ left: item.left, top: item.top },
								{ left: item.left - this.blockSize, top: item.top - this.blockSize }, // 左上
								{ left: item.left, top: item.top - this.blockSize },									// 上
								{ left: item.left + this.blockSize, top: item.top - this.blockSize },	// 右上
								{ left: item.left + this.blockSize, top: item.top },									// 右
								{ left: item.left + this.blockSize, top: item.top + this.blockSize }, // 右下
								{ left: item.left, top: item.top + this.blockSize }, 									// 下
								{ left: item.left - this.blockSize, top: item.top + this.blockSize },	// 左下
								{ left: item.left - this.blockSize, top: item.top },									// 左
								{ left: item.left - this.blockSize * 2, top: item.top },							// 左左
								{ left: item.left + this.blockSize * 2, top: item.top },							// 右右
								{ left: item.left, top: item.top - this.blockSize * 2 },							// 上上
								{ left: item.left, top: item.top + this.blockSize * 2 },							// 下下
							]
							cleanBlockList.forEach(item2 => {
								const finIndex = this.gameListData.findIndex(a => a.left === item2.left && a.top === item2.top)
								if (finIndex >= 0) {
									const item1Copy = JSON.parse(JSON.stringify(this.gameListData[finIndex]))
									this.gameListData[finIndex].show = false
									this.gameListData[finIndex].option = 0
									// 如果找到的这个是个功能方块，则继续处理
									if (item1Copy.option !== 0) {
										// this.abilityClean(item1Copy, parentAtIndex)
									}
								}
							})
						} else {
							// 普通消除
							this.gameListData[parentAtIndex].show = false
							this.gameListData[parentAtIndex].option = 0
						}
					},
					
					// 重新绘制地图
					// 把消除的方块上面的，都落下来
					// list 已经清空的方块
					async drawMap(list = []) {
						// 因为清空有动画效果，所以需要价格延时，看起来方块消失完再出发
						await this.sleep(this.downSleep * 1000)
						this.isDown = true
						// 1、先把list处理一下，left值相等，取top值最大的那个
						const map = new Map()
						list.forEach(item => {
							const left = item.left
							if(!map.has(left) || item.top > map.get(left).top) {
								map.set(left, item)
							}
						})
						const listFilter = Array.from(map.values())
						// 2、所有需要下落的方块，也就是获取listFilter每个消除砖块上面所有show=true的砖块
						const fallList = []
						for (let i = 0; i < listFilter.length; i++) {
							const item = listFilter[i]
							// 这个要x2, 因为需要找每个消除砖块上面所有，包括地图外面的
							for (let j = 1; j <= this.row * 2; j++) {
								// 上面那个方块的top值
								const top = item.top - j * this.blockSize
								// 获取上面那个方块的信息
								const upItem = this.gameListData.find(d => d.left === item.left && d.top === top)
								// 边界判断，并且显示的水果
								if (upItem && upItem.show) {
									fallList.push(upItem)
								}
							}
						}
						// 3、开始下落
						// 计算每个下落方块的下面，有多少个空格，有多少个就下落多少个
						for (let i = 0; i < fallList.length; i++) {
							const item = fallList[i]
							let nullNum = 0
							// 这里x2，第二步一样操作
							for(let j = 1; j < this.row * 2; j++) {
								const nullTop = item.top + j * this.blockSize
								const nullItem = this.gameListData.find(a => a.left === item.left && a.top === nullTop)
								// 如果这块不存在，说明到底了，就退出
								if (!nullItem) {
									break
								}
								if (nullItem && nullItem.show === false) {
									nullNum++
								}
							}
							if (nullNum > 0) {
								const top = item.top + nullNum * this.blockSize
								const left = item.left
								const targetItem = this.gameListData.find(a => a.left === left && a.top === top)
								const temp = targetItem.top
								targetItem.top = item.top
								item.top = temp
							}
						}
						
						// 动画延迟，等待下落完成
						await this.sleep(this.downSleep * 1000)
						this.isDown = false
						// 每次下落之后，都要再次进行清除处理
						this.clearBlock()
					},
					
					// 延时，主要用于代码的执行顺序控制，以达到动画与用户操作之间的时间控制
					sleep(time) {
						return new Promise((resolve) => {
							setTimeout(() => {
								resolve()
							}, time)
						})
					},
					
					// 将包含相同元素的多个组合并成一个组，并去除重复元素
					mergeAndDeduplicateGroups(groups) {
						// 创建一个映射来跟踪每个元素属于哪些组
						const elementToGroupIndices = new Map()
						// 1、记录每个元素出现在哪些组中
						groups.forEach((group, groupIndex) => {
							group.forEach(item => {
								// 创建唯一标识
								const key = `${item.type}-${item.left}-${item.top}`
								if (!elementToGroupIndices.has(key)) {
									elementToGroupIndices.set(key, new Set())
								}
								elementToGroupIndices.get(key).add(groupIndex)
							})
						})
						// 2、使用并查集来合并有交集的组
						// unionFind数组记录每个组所属的根组
						const unionFind = new Array(groups.length)
						for (let i = 0; i < groups.length; i++) {
							unionFind[i] = i
						}
						// 查找方法：查找元素属于哪个集合
						function find(x) {
							if (unionFind[x] !== x) {
								unionFind[x] = find(unionFind[x])
							}
							return unionFind[x]
						}
						// 合并方法：合并两个集合
						function union(x, y) {
							// 分别找到两个组所在的根集合
							const rootX = find(x)
							const rootY = find(y)
							// 如果两个组不在同一个集合中，才需要合并
							if (rootX !== rootY) {
								unionFind[rootY] = rootX
							}
						}
						// 3、合并有共同元素的组
						for (const groupIndices of elementToGroupIndices.values()) {
							// 将Set转换为数组
							const indicesArray = Array.from(groupIndices)
							if (indicesArray.length > 1) {
								// 将这些有交集的组合并到同一个集合中
								for (let i = 1; i < indicesArray.length; i++) {
									union(indicesArray[0], indicesArray[i])
								}
							}
						}
						// 4、重新组织分组并去重
						// 使用Map来存储合并后的组，key是根组索引，value是去重后的元素Map
						const mergedGroupsMap = new Map()
						for (let i = 0; i < groups.length; i++) {
							// 找到当前组所属的根集合
							const root = find(i)
							// 如果这个根集合还没有在mergedGroupsMap中，初始化它
							if (!mergedGroupsMap.has(root)) {
								mergedGroupsMap.set(root, new Map())
							}
							const currentGroup = groups[i]
							const mergedGroupMap = mergedGroupsMap.get(root)
							// 添加当前组的所有元素到合并组中（使用Map自动去重）
							currentGroup.forEach(item => {
								const key = `${item.type}-${item.left}-${item.top}`
								// 如果这个元素还没有在合并后的组中出现过，添加它
								if (!mergedGroupMap.has(key)) {
									mergedGroupMap.set(key, { ...item })
								}
							})
						}
						// 5、转换为最终的二维数组格式
						const result = []
						for (const groupMap of mergedGroupsMap.values()) {
							const groupArray = Array.from(groupMap.values())
							result.push(groupArray)
						}
						return result
					}
				}
			})
		</script>
	</body>
</html>